﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace DDD {
    /// <summary>
    /// シェーダープログラム
    /// </summary>
    /// <remarks>
    /// <see cref="ShaderProgram"/>はシェーダーを表します。
    /// Render()関数はHW依存なので拡張メソッドとして<see cref="Graphics3D"/>で追加される予定
    /// <para>
    /// コンパイル＆リンク
    /// シェーダーはソースプログラム(文字列)の状態でロードされコンパイル＆リンクして初めて使用可能になります。
    /// そのタイミングは規定されていませんがDDDではレンダリングコンテキストフリーシーンコンストラクションを標榜している以上、
    /// コンテキストが必要なコンパイル＆リンクが最後のRender()まで行えません。
    /// 従ってシェーダー中で使用される変数名などがレンダリングしてみるまで取得できません。
    /// これではあまりにも不便なので<see cref="Graphics3D"/>クラスは拡張メソッドとして
    /// <see cref="ShaderProgram"/>クラスにコンパイルとリンクを行う<dic>"Render"</dic>メソッドを追加します。
    /// このメソッドを呼ぶにはコンテキストが必要です。
    /// このメソッドの呼び出しが成功すればシェーダーの変数宣言情報にアクセスできるようになります。
    /// </para>
    /// <para>
    /// シェーダー変数
    /// シェーダーで宣言された変数にはUniform変数とAttriute変数があります。
    /// Attribute変数は頂点毎に変化する値で座標位置や法線、テクスチャー座標などが相当します。
    /// Uniform変数はシェーダーの実行中に変化しないいわばグローバル変数を定義します。
    /// 両者は異なった方法でシェーダーに送られます。
    /// </para>
    /// <para>
    /// Attribute変数
    /// Attirute変数をシェーダーに送るには<see cref="VertexBuffer"/>クラスの<see cref="VertexBuffer.AddVertexArray"/>メソッドを使用します。
    /// ここでセットされた<see cref="VertexArray"/>オブジェクトから頂点データが取り出されシェーダーの指定のAttribute変数に送られます。
    /// </para>
    /// <para>
    /// Uniform変数
    /// Uniform変数をシェーダーに送るには<see cref="Appearance"/>クラスの<see cref="Appearance.AddUniformArray"/>メソッドを使用します。
    /// <see cref="UniformArray"/>クラスは複数のUniform変数を格納するコンテナクラスです。
    /// ここでセットされた<see cref="UniformArray"/>オブジェクトが指定のUniform変数に送られます。
    /// </para>
    /// </remarks>
    public class ShaderProgram : Object {

        #region Field
        VertexShader vertShader;
        FragmentShader fragShader;
        ShaderDeclaration[] attributes;
        ShaderDeclaration[] uniforms;
        string[] feedbacks;
        string log;
        #endregion


        #region Constructor
        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <remarks>
        /// 指定の頂点シェーダーとフラグメントシェーダーを持つシェーダープログラムを作成します。
        /// この段階ではまだコンパイルとリンクは行われません。
        /// トランスフォームフィードバックを使用する時はフラグメントシェーダーは省略可能でnullを指定します。
        /// </remarks>
        /// <param name="vert"><see cref="VertexShader"/>オブジェクト(null不可)</param>
        /// <param name="frag"><see cref="FragmentShader"/>オブジェクト(null可)</param>
        public ShaderProgram (VertexShader vert, FragmentShader frag) {
            if (vert == null) {
                throw new ArgumentNullException ("Vertex shader is null");
            }
            this.vertShader = vert;
            this.fragShader = frag;
            this.attributes = new ShaderDeclaration[0];
            this.uniforms = new ShaderDeclaration[0];
            this.feedbacks = new string[0];
            this.log = "";
        }

        #endregion

        #region Property

        /// <summary>
        /// バーテックス・シェーダー
        /// </summary>
        public VertexShader VertexShader {
            get { return vertShader; }
        }

        /// <summary>
        /// フラグメント・シェーダー
        /// </summary>
        public FragmentShader FragmentShader {
            get { return fragShader; }
        }

        /// <summary>
        /// シェーダー中で宣言されたAttribute変数の総数
        /// </summary>
        /// <remarks>
        /// コンパイル＆リンクを行うまでは0が返ります。
        /// </remarks>
        public int AttributeCount {
            get { return attributes.Length; }
        }

        /// <summary>
        /// シェーダー中で宣言されたUniform変数の総数
        /// </summary>
        /// <remarks>
        /// コンパイル＆リンクを行うまでは0が返ります。
        /// </remarks>
        public int UniformCount {
            get { return uniforms.Length; }
        }

        /// <summary>
        /// 使用するフィードバックVarying変数の総数
        /// </summary>
        public int FeedbackVaryingCount {
            get { return feedbacks.Length; }
        }

        /// <summary>
        /// フィードバックを使用するかどうかのフラグ
        /// </summary>
        public bool IsFeedbackEnabled {
            get { return (feedbacks.Length > 0) ? true : false; }
        }

        /// <summary>
        /// 全Attribute変数宣言を列挙する列挙子
        /// </summary>
        /// <remarks>
        /// コンパイル＆リンクを行うまでは列挙できません。
        /// </remarks>
        public IEnumerable<ShaderDeclaration> Attributes {
            get { return attributes; }
        }

        /// <summary>
        /// 全Uniform変数の列挙子
        /// </summary>
        /// <remarks>
        /// コンパイル＆リンクを行うまでは列挙できません。
        /// </remarks>
        public IEnumerable<ShaderDeclaration> Uniforms {
            get { return uniforms; }
        }

        /// <summary>
        /// 全フィードバック変数を列挙する列挙子
        /// </summary>
        public IEnumerable<string> FeedbackVaryings {
            get { return feedbacks.Where (x => x != null); }
        }



        /// <summary>
        /// リンク・ログ
        /// </summary>
        /// <remarks>
        /// リンクを行った後にログが記録されます。
        /// 何らかのエラーが起きた場合にチェックすると原因がわかる可能性があります。
        /// </remarks>
        public string LinkLog {
            get { return log; }
        }

        /// <summary>
        /// このオブジェクトが参照する全DDDオブジェクトを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 参照にnullは含まれません。同一のオブジェクトが複数出現する可能性があります。
        /// </remarks>
        public override IEnumerable<Object> References {
            get {
                var objs = base.References.ToList ();
                objs.Add (vertShader);
                objs.Add (fragShader);
                objs.RemoveAll (x => x == null);
                return objs;
            }
        }
        #endregion

        #region Method

        /// <summary>
        /// 指定の名前のAttribute変数の宣言の取得します
        /// </summary>
        /// <remarks>
        /// コンパイル＆リンクを行うまでは列挙できません。
        /// </remarks>
        /// <param name="name">変数名</param>
        /// <returns>シェーダー変数宣言</returns>
        public ShaderDeclaration GetAttribute (string name) {
            if (attributes.Length == 0) {
                throw new InvalidOperationException ("No attributes was found (not linked?)");
            }
            if (!attributes.Any (x => x.Name == name)) {
                throw new ArgumentException ("Such attribute name was not found");
            }

            return attributes.First (x => x.Name == name);
        }

        /// <summary>
        /// Uniform変数の宣言の取得します
        /// </summary>
        /// <remarks>
        /// コンパイル＆リンクを行うまでは列挙できません。
        /// </remarks>
        /// <param name="name">変数名</param>
        /// <returns>シェーダー変数宣言</returns>
        public ShaderDeclaration GetUniform (string name) {
            if (uniforms.Length == 0) {
                throw new InvalidOperationException ("No Uniforms was found (not linked?)");
            }
            if (!uniforms.Any (x => x.Name == name)) {
                throw new ArgumentException ("Such uniforms name was not found");
            }
            return uniforms.First (x => x.Name == name);
        }


        /// <summary>
        /// リンクログの変更
        /// </summary>
        /// <param name="log">ログ</param>
        internal void SetLog (string log) {
            this.log = log;
        }

        /// <summary>
        /// Attribute変数宣言のセット
        /// </summary>
        /// <param name="attributes">Attribute変数宣言の配列</param>
        internal void SetAttributes (ShaderDeclaration[] attributes) {
            this.attributes = (ShaderDeclaration[])attributes.Clone ();
        }

        /// <summary>
        /// Uniform変数宣言のセット
        /// </summary>
        /// <param name="uniforms">Uniform変数宣言の配列</param>
        internal void SetUniforms (ShaderDeclaration[] uniforms) {
            this.uniforms = (ShaderDeclaration[])uniforms.Clone ();
        }

        /// <summary>
        /// トランスフォームフィードバック機能を有効にします
        /// </summary>
        /// <param name="enable">有効・無効</param>
        /// <param name="count">使用するVarying変数の総数</param>
        public void SetTransformFeedbackEnable (bool enable, int count) {
            if (!enable) {
                this.feedbacks = new string[0];
                return;
            }
            if (count <= 0 || count > 128) {
                throw new ArgumentException ("Count is invalid");
            }

            this.feedbacks = new string[count];
        }

        /// <summary>
        /// フィードバックVarying変数のセット
        /// </summary>
        /// <param name="index">インデックス</param>
        /// <param name="name">Varying変数名</param>
        public void SetFeedbackVarying (int index, string name) {
            if (index < 0 || index >= FeedbackVaryingCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }
            if (feedbacks.Contains (name)) {
                throw new ArgumentException ("Name is already registered");
            }

            this.feedbacks[index] = name;
        }

        /// <summary>
        /// フィードバックVarying変数の削除
        /// </summary>
        /// <param name="name">Varying変数名</param>
        /// <returns>削除したらtrue,そうでなければfalse</returns>
        public bool RemoveFeedbackVarying (string name) {
            for (var i = 0; i < FeedbackVaryingCount; i++) {
                if (feedbacks[i] == name) {
                    this.feedbacks[i] = null;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// フィードバックVarying変数の取得
        /// </summary>
        /// <param name="index">インデックス</param>
        /// <returns></returns>
        public string GetFeedbackVarying (int index) {
            if (index < 0 || index >= FeedbackVaryingCount) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }

            return feedbacks[index];
        }


        /// <summary>
        /// 現在のオブジェクトの状態を表す文字列を返します。
        /// </summary>
        /// <returns>現在のオブジェクトを説明する文字列</returns>
        public override string ToString () {
            return string.Format ("ShaderProgram:  Vert={1}, Frag={2}", VertexShader, FragmentShader);
        }
        #endregion

    }
}
